#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/ioport.h>
#include <linux/io.h>

#define GPIO_REG_BASE		0xC0010000
#define GPIO_MAP_SIZE		0x2000
#define GPIO_B_OFFSET		0xB000
#define GPIO_C_OFFSET		0xC000
#define GPIO_OUT_OFFSET		0x0
#define GPIO_OUTEN_OFFSET	0x4

#define LED_NR	4

struct led_desc {
	unsigned int mask;
	unsigned int out_offset;
	void __iomem *vir_out_addr;
	unsigned int outen_offset;
	void __iomem *vir_outen_addr;
};

static struct led_desc leds[] = {
	[0] = {
		.mask		= 1 << 26,
		.out_offset	= GPIO_B_OFFSET + GPIO_OUT_OFFSET,
		.outen_offset	= GPIO_B_OFFSET + GPIO_OUTEN_OFFSET,
	},

	[1] = {
		.mask		= 1 << 11,
		.out_offset	= GPIO_C_OFFSET + GPIO_OUT_OFFSET,
		.outen_offset	= GPIO_C_OFFSET + GPIO_OUTEN_OFFSET,
	},

	[2] = {
		.mask		= 1 << 7,
		.out_offset	= GPIO_C_OFFSET + GPIO_OUT_OFFSET,
		.outen_offset	= GPIO_C_OFFSET + GPIO_OUTEN_OFFSET,
	},

	[3] = {
		.mask		= 1 << 12,
		.out_offset	= GPIO_C_OFFSET + GPIO_OUT_OFFSET,
		.outen_offset	= GPIO_C_OFFSET + GPIO_OUTEN_OFFSET,
	},
};

static struct resource *led_res;
static void __iomem *led_vir_base;

static dev_t led_major;
static struct class *led_class;

static struct led_priv {
	dev_t device_id;
	struct led_desc desc;
	int status;
	struct cdev led_cdev;
	struct device *led_device;
} led_dat[LED_NR];

static void led_enable(struct led_desc *desc)
{
	int val;

	val = readl(desc->vir_outen_addr);
	val |= desc->mask;
	writel(val, desc->vir_outen_addr);
}

static void led_op(struct led_desc *desc, int status)
{
	unsigned int val;

	val = readl(desc->vir_out_addr);

	if (status) {
		val &= ~(desc->mask);
	} else {
		val |= desc->mask;
	}

	writel(val, desc->vir_out_addr);
}

static int led_open(struct inode *inode, struct file *filp)
{
	struct led_priv *dat;

	dat = container_of(inode->i_cdev, struct led_priv, led_cdev);
	filp->private_data = (void *)dat;

	return 0;
}

ssize_t led_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
	unsigned int val;
	int ret;
	char cmd[8];
	struct led_priv *led_dat;

	ret = copy_from_user(cmd, buf, 1);
	if (ret < 0) {
		printk("copy_from_user failed.\n");
		ret = -EINVAL;
	}

	led_dat = (struct led_priv *)filp->private_data;

	val = cmd[0] - '0';
	led_op(&led_dat->desc, val);

	ret = size;

	return ret;
}

static struct file_operations led_ops = {
	.open	= led_open,
	.write	= led_write,
};

static int __init led_map_init(void)
{
	int ret;
	int i;

	ret = alloc_chrdev_region(&led_major, 0, LED_NR, "led_dev");
	if (ret < 0) {
		printk("led is register_chrdev_region error.\n");
		goto err_1;
	}

	led_class = class_create(THIS_MODULE, "led_class");
	if (IS_ERR(led_class)) {
		printk("led class_create error.\n");
		goto err_2;
	}

	led_res = request_mem_region(GPIO_REG_BASE, GPIO_MAP_SIZE, "LED gpio port");
	if (NULL == led_res)
		printk("Cannot request iomem region for my GPIO port\n");

	led_vir_base = ioremap(GPIO_REG_BASE, GPIO_MAP_SIZE);
	if (NULL == led_vir_base) {
		printk("Cannot ioremap for: 0x%x\n", GPIO_REG_BASE);
		ret = -EIO;
		goto err_3;
	}

	for (i = 0; i < LED_NR; i++) {
		led_dat[i].desc = leds[i];
		led_dat[i].desc.vir_out_addr = led_vir_base + leds[i].out_offset;
		led_dat[i].desc.vir_outen_addr = led_vir_base + leds[i].outen_offset;
		led_enable(&led_dat[i].desc);

		led_dat[i].device_id = led_major + i;
		cdev_init(&led_dat[i].led_cdev, &led_ops);
		ret = cdev_add(&led_dat[i].led_cdev, led_dat[i].device_id, 1);
		if (ret < 0) {
			printk("Cdev add error.\n");
		}
		device_create(led_class, NULL, led_dat[i].device_id, NULL, "led%d", i);
		
		led_op(&led_dat[i].desc, 1);
		msleep(20);
		led_op(&led_dat[i].desc, 0);
	}

	return 0;

err_3:
	class_destroy(led_class);
err_2:
	unregister_chrdev_region(led_major, LED_NR);
err_1:
	return ret;
}

static void __exit led_map_exit(void)
{
	int i;

	for (i = LED_NR - 1; i >= 0; i--) {
		led_op(&led_dat[i].desc, 1);
		msleep(20);
		led_op(&led_dat[i].desc, 0);

		device_destroy(led_class, led_dat[i].device_id);
		cdev_del(&led_dat[i].led_cdev);
	}

	iounmap(led_vir_base);
	if (led_res)
		release_mem_region(GPIO_REG_BASE, GPIO_MAP_SIZE);

	class_destroy(led_class);
	unregister_chrdev_region(led_major, LED_NR);
}

module_init(led_map_init);
module_exit(led_map_exit);
MODULE_AUTHOR("Yuyaowen");
MODULE_LICENSE("GPL");
